Skip to content

feat(signing): L1 signing — RFC 9421, KMS providers, JWKS, webhook verification#32

Open
MichielDean wants to merge 21 commits into
mainfrom
feat/l1-signing-track4
Open

feat(signing): L1 signing — RFC 9421, KMS providers, JWKS, webhook verification#32
MichielDean wants to merge 21 commits into
mainfrom
feat/l1-signing-track4

Conversation

@MichielDean

Copy link
Copy Markdown
Collaborator

Summary

Track 4 (L1 Signing) — complete RFC 9421 message signing and verification for the AdCP Java SDK.

Core SPI (adcp module)

  • SigningProvider / VerificationKeyResolver — SPI interfaces via META-INF/services/
  • SigningContext — tenant-aware key selection (D22): AdcpUse use(), @Nullable TenantId tenant(), @Nullable PrincipalRef principal()
  • AdcpUse enum — REQUEST_SIGNING, WEBHOOK_SIGNING with wire name mapping
  • VerificationKeyLookup — sealed interface: Found(key, tenant, principal) / Missing(kid)
  • VerificationResult — sealed interface: Valid / Invalid(errorCode, reason, failedStep)
  • VerificationException — typed errors matching webhook_signature_* taxonomy

RFC 9421 Canonicalizer (adcp-server module)

  • Hand-rolled implementation per RFC 9421 §2.5 (no org.tomitribe:http-signatures dependency)
  • 93 conformance tests against AdCP test vectors (webhook signing positive/negative, request signing, URL canonicalization)
  • Rfc9421Signer — produces Signature-Input, Signature, Content-Digest headers
  • Rfc9421Verifier — full 13-step verification checklist
  • AdcpSignatureProfile — required components, algorithms, tags, replay window enforcement
  • HeaderNormalizer — RFC 9421 header name/value normalization
  • ContentDigest — RFC 9530 SHA-256/512 content digest
  • IDNA/UTS#46 hostname canonicalization

Signing Providers

  • InProcessSigningProvider — JCA Ed25519, ES256, ES384 (no Bouncy Castle in core)
  • InProcessKeyGenerator — Ed25519/ES256/ES384 keypair generation
  • AwsKmsSigningProvider — AWS KMS with lazy-init, tripwire verification, ECDSA DER-to-raw conversion
  • GcpKmsSigningProvider — GCP KMS with lazy-init, tripwire, service account credentials
  • BouncyCastleSigningProvider — stub (FIPS environments only, UnsupportedOperationException)

Verification & Key Resolution

  • JwksVerificationKeyResolver — SPI entry point
  • StaticJwksResolver — in-memory from pre-loaded JWKS
  • CachingJwksResolver — HTTP-fetching with 30s cooldown, single-flight dedup
  • BrandJsonJwksResolver — 3-hop resolution (brand.json → agent → jwks_uri)
  • SsrfJwksUriValidator — HTTPS enforcement, private IP rejection, DNS rebinding defense
  • JwkParser — Ed25519, EC (P-256/P-384), RSA JWK parsing with adcp_use and key_ops validation

Webhook Signing

  • WebhookSigner seam interface for Track 6 (async-l3)
  • DefaultWebhookSigner — implementation using Rfc9421Signer
  • LegacyHmacWebhookVerifier — deprecated HMAC-SHA256 (removed in AdCP 4.0), timing-safe comparison, rotation support
  • StandardWebhooksVerifier — Svix/Resend v1 interop
  • WebhookChallenge + WebhookChallengeVerifier — endpoint proof-of-control

Key Management

  • InMemoryReplayStore — nonce dedup with 300s TTL eviction
  • InMemoryRevocationStore — revoked kid tracking with staleness detection
  • CachingRevocationChecker — JWS-verified revocation list fetcher with grace window
  • SigningKeyGenerator — Ed25519/ES256/ES384 keypair generation with JWK output
  • KeyOriginConsistencyCheck — ADCP #3690 eTLD+1 key origin verification
  • ETldPlusOne — public suffix extraction utility

JWS Verification

  • JwsVerifier — compact and JSON general JWS verification
  • JwsDocument — parsed JWS record

CLI

  • signing-probe subcommand — pre-deploy KMS connectivity and key usability check

Modules

  • adcp — core SPI interfaces
  • adcp-server — canonicalizer, providers, verification, webhook
  • adcp-signing-aws-kms — AWS KMS provider (real implementation)
  • adcp-signing-gcp-kms — GCP KMS provider (real implementation)
  • adcp-signing-bouncycastle — BC FIPS provider (stub)
  • adcp-cli — signing-probe command

Tests

226+ tests across all signing modules, including full conformance coverage against AdCP webhook signing and request signing test vectors.

Design Decisions

  • Hand-rolled canonicalizer (not org.tomitribe:http-signatures) — Cavage draft ≠ RFC 9421
  • No Bouncy Castle in core — JDK 21 has Ed25519 natively; BC only in optional FIPS module
  • Lazy-init for KMS — no gRPC retries on boot; pre-deploy probe is a separate CLI
  • Tripwire verification — committed SPKI fingerprint compared against KMS-returned key at first sign
  • ECDSA DER-to-raw — AWS/GCP KMS return DER-encoded signatures; RFC 9421 requires raw (r||s) format
  • SigningContext-based API (D22) — no single-arg forUse(); tenant and principal nullable until v0.3

Known Limitations

  • AccountStoreSigningContext tenant resolution wiring deferred to Track 5 (v0.3)
  • Postgres-backed ReplayStore deferred to Track 6 (async-l3)
  • adcp-keygen CLI wrapper not yet added (trivial, can land anytime)

Lobsterdog Contributors added 18 commits June 16, 2026 22:28
…verification round-trip tests

Track 4 (L1 Signing) implementation:

- InProcessSigningProvider: JCA Ed25519/ES256/ES384 signing via
  SigningProvider SPI, delegates to Rfc9421Canonicalizer for signature
  base construction
- InProcessKeyGenerator: test keypair generation utility (Ed25519,
  ES256, ES384) for development/testing
- InProcessVerificationProvider: verification path using Rfc9421Verifier
  with AdCP profile validation
- WebhookSigner/DefaultWebhookSigner: seam interface for Track 6
  (async-l3) webhook signing, applies AdCP webhook-signing profile
- WebhookSigningResult: record for signed webhook headers
- Tests: signing/verification round-trip for Ed25519 and ES256,
  key generation, webhook signer seam
…lidation, and brand.json walk

- JwkParser: parse Ed25519, EC (P-256/P-384), and RSA JWKs into VerificationKey
- StaticJwksResolver: in-memory kid lookup from pre-loaded JWKS
- CachingJwksResolver: HTTP-fetching with 30s cooldown, single-flight dedup
- BrandJsonJwksResolver: 3-hop resolution (brand.json → agent → jwks_uri)
- SsrfJwksUriValidator: HTTPS enforcement, private IP rejection, redirect:manual
- JwksDocument: parsed JWKS with per-kid index and lastFetched timestamp
- JwksResolutionException: unchecked exception for JWKS fetch failures
- SsrfBlockedException: made constructor public for JWKS validator tests
@MichielDean MichielDean requested a review from bokelley as a code owner June 17, 2026 20:44
@gitguardian

gitguardian Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 3 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
34069021 Triggered Generic High Entropy Secret f3d9c7b adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java View secret
34069022 Triggered Generic High Entropy Secret f3d9c7b adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java View secret
34069023 Triggered Generic High Entropy Secret f3d9c7b adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@aao-ipr-bot

aao-ipr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

@aao-ipr-bot

aao-ipr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

The compliance test vectors in adcp-server/src/test/resources/compliance/
contain intentionally public test keys with _private_d_for_test_only
fields. These are for signer/verifier round-trip conformance testing and
MUST NOT be used in production. The keys.json files carry explicit
warnings in their _WARNING and  fields.
@aao-ipr-bot

aao-ipr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

…h presence)

The .gitguardian file only takes effect when present on the default branch.
For now, the test key findings will be marked as false positives via
the GitGuardian dashboard. The keys.json files contain intentionally
public test keys from the AdCP compliance suite with explicit warnings.
@aao-ipr-bot

aao-ipr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the large signing implementation. I reviewed the verifier, signing providers, JWKS/brand.json resolution, revocation handling, and webhook helpers. I think this needs another pass before merge; the targeted tests pass locally, but there are several correctness/security issues that the current coverage does not catch.

Findings:

  1. [P1] Malformed signature inputs can throw instead of returning VerificationResult.Invalid.
    adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Verifier.java:100 parses created/expires with Long.parseLong outside a catch, and extractSignatureBytes can throw from Base64.getUrlDecoder().decode(...) at line 364. A bad remote header can therefore become a verifier exception/500 instead of a signature rejection. Please keep all wire-format failures on the VerificationResult.Invalid path.

  2. [P1] Empty webhook bodies cannot be signed correctly.
    Webhook signing requires content-digest, but DefaultWebhookSigner.java:44 and InProcessSigningProvider.java:77 only add it when body.length > 0. For WEBHOOK_SIGNING, the covered component list still includes content-digest, so empty-payload webhooks either fail canonicalization or skip digest validation. The digest of an empty byte array is still a valid required Content-Digest value.

  3. [P1] brand.json JWKS URI rotation is broken.
    In BrandJsonJwksResolver.java, selectedJwksUri = jwksUri is assigned at line 214 before comparing at line 218, so the resolver replacement condition never detects a changed URI once innerResolver exists. If brand.json rotates to a new jwks_uri, the process can keep using the stale resolver until restart. Capture the previous selected URI before assignment, or compare against the existing resolver state.

  4. [P2] Standard Webhooks secret padding is incorrect.
    StandardWebhooksVerifier.java:65 uses "=".repeat((-payload.length()) % 4). Java preserves the negative remainder, so lengths where padding is needed can throw IllegalArgumentException: count is negative. Use (4 - payload.length() % 4) % 4 instead, and add an unpadded secret test for non-multiple-of-4 lengths.

  5. [P2] Revocation stale handling does not match the result API.
    CachingRevocationChecker.check returns RevocationResult, and the class docs say stale lists return RevocationResult.Stale, but ensureFresh() throws RevocationListStaleException from line 132. Callers using the sealed result API will not see the advertised Stale result. Please either return RevocationResult.Stale from check or change the API/docs/tests to make stale an exception path consistently.

Local verification run:

./gradlew :adcp-server:test :adcp-signing-aws-kms:test :adcp-signing-gcp-kms:test
BUILD SUCCESSFUL in 2m 57s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants